import copy
import json
from json import JSONDecodeError
import warnings
from typing import Dict, Optional, Union
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlparse

from db_exporter.conf import db_type_dict
from db_exporter.exception import DBExportException
from db_exporter.utils import b64encode, b64decode
from db_exporter.utils.module_loading import import_string


def _parse_netloc_to_hostname(uri_parts):
    """Parse a URI string to get correct Hostname."""
    hostname = unquote(uri_parts.hostname or '')
    if '/' in hostname:
        hostname = uri_parts.netloc
        if "@" in hostname:
            hostname = hostname.rsplit("@", 1)[1]
        if ":" in hostname:
            hostname = hostname.split(":", 1)[0]
        hostname = unquote(hostname)
    return hostname


class Connection(object):
    '''
    # scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
    # file:///cffex/trade/tmp/a.txt
    # file://172.22.128.1/cffex/trade/tmp/a.txt
    # ftp://user:myword@exchftp.cffe.net:23/你好/test/a.txt
    '''

    def __init__(
            self,
            conn_id: Optional[str] = None,
            conn_type: Optional[str] = None,
            description: Optional[str] = None,
            host: Optional[str] = None,
            login: Optional[str] = None,
            password: Optional[str] = None,
            schema: Optional[str] = None,
            port: Optional[int] = None,
            extra: Optional[Union[str, dict]] = None,
            uri: Optional[str] = None,
    ):
        super().__init__()
        self.EXTRA_KEY = "extra"
        self.conn_id = conn_id
        self.description = description
        if extra and not isinstance(extra, str):
            extra = json.dumps(extra)
        if uri and (conn_type or host or login or password or schema or port or extra):
            raise DBExportException(
                "You must create an object using the URI or individual values "
                "(conn_type, host, login, password, schema, port or extra)."
                "You can't mix these two ways to create this object."
            )
        if uri:
            self._parse_from_uri(uri)
        else:
            self.conn_type = conn_type
            self.host = host
            self.login = login
            # self.password = b64decode(password)
            self.password = password
            self.schema = schema
            self.port = port
            self.extra = extra
        if self.extra:
            self._validate_extra(self.extra, self.conn_id)

        if self.password:
            self.mask_secret(self.password)

    @staticmethod
    def _normalize_conn_type(conn_type):
        if conn_type == 'postgresql':
            conn_type = 'postgres'
        elif '-' in conn_type:
            conn_type = conn_type.replace('-', '_')
        return conn_type

    def mask_secret(self, password):
        self.password = password

    @staticmethod
    def unmask_secret(password_security):
        return b64decode(password_security)

    def _validate_extra(self, extra, conn_id):
        pass

    def _parse_from_uri(self, uri: str):
        uri_parts = urlparse(uri)
        self.conn_type = uri_parts.scheme
        # self.conn_type = self._normalize_conn_type(conn_type)
        self.host = _parse_netloc_to_hostname(uri_parts)
        quoted_schema = uri_parts.path[1:]
        self.schema = unquote(quoted_schema) if quoted_schema else quoted_schema
        self.login = unquote(uri_parts.username) if uri_parts.username else uri_parts.username
        self.password = unquote(uri_parts.password) if uri_parts.password else uri_parts.password
        self.port = uri_parts.port
        if uri_parts.query:
            query = dict(parse_qsl(uri_parts.query, keep_blank_values=True))
            if self.EXTRA_KEY in query:
                self.extra = query[self.EXTRA_KEY]
            else:
                self.extra = json.dumps(query)

    def __repr__(self):
        return self.conn_id or ''

    def test_connection(self):
        """Calls out get_hook method and executes test_connection method on that."""
        status, message = False, 'unsupported'
        return status, message

    @property
    def extra_dejson(self) -> Dict:
        """Returns the extra property by deserializing json."""
        obj = {}
        if self.extra:
            try:
                obj = json.loads(self.extra)
            except JSONDecodeError:
                self.log.exception("Failed parsing the json for conn_id %s", self.conn_id)

        return obj

    @classmethod
    def from_json(cls, value, conn_id=None) -> 'Connection':
        kwargs = json.loads(value)
        extra = kwargs.pop('extra', None)
        if extra:
            kwargs['extra'] = extra if isinstance(extra, str) else json.dumps(extra)
        conn_type = kwargs.pop('conn_type', None)
        if conn_type:
            kwargs['conn_type'] = cls._normalize_conn_type(conn_type)
        port = kwargs.pop('port', None)
        if port:
            try:
                kwargs['port'] = int(port)
            except ValueError:
                raise ValueError(f"Expected integer value for `port`, but got {port!r} instead.")
        return Connection(conn_id=conn_id, **kwargs)

    @classmethod
    def get_connection_from_id(cls, conn_id=None) -> 'Connection':
        # TODO: 根据conn_id获取参数信息

        conn_para = {}
        conn_para["password"] = b64decode(conn_para["password"])

        return Connection(conn_id=conn_id, **conn_para)

    def get_plugin(self, *, hook_params=None):
        """Return hook based on conn_type"""
        plugin_class = db_type_dict[self.conn_type]["class"]
        if plugin_class is None:
            raise DBExportException(f'Unknown hook type "{self.conn_type}", please check core.db_type_dict')
        if hook_params is None:
            hook_params = {"connection": self}
        return plugin_class(**{"conn_id": self.conn_id}, **hook_params)
